using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;

namespace TwoPageView3
{
    public class ThumbnailCacheEntry
    {
        private DateTime mDateTime;
        private long mFileSize;
        private int bitmapBytes = 0;
        private Size mOriginalWH;
        private BitmapImage bitmapImage;
        private const int CACHE_IMAGE_SIZE_MAX = 1024 * 1024;
        private const int JPEG_ENCODER_QUALITY = 90;

        public DateTime DateTime
        {
            get { return mDateTime; }
            set { mDateTime = value; }
        }
        public long FileSize
        {
            get { return mFileSize; }
            set { mFileSize = value; }
        }
        public Size OriginalWH
        {
            get { return mOriginalWH; }
            set { mOriginalWH = value; }
        }
        public BitmapImage ThumbnailBitmap
        {
            get { return bitmapImage; }
            set { bitmapImage = value; }
        }

        public ThumbnailCacheEntry(DateTime dateTime, long fileSize, BitmapImage bitmap, Size originalWH)
        {
            mDateTime = dateTime;
            mFileSize = fileSize;
            mOriginalWH = originalWH;
            bitmapImage = bitmap;
        }

        public void Write(Stream stream, out long streamPosition)
        {
            if (null == stream) {
                throw new ArgumentNullException("stream");
            }
            using (var bw = new BinaryWriter(stream)) {
                bw.Write(mDateTime.ToBinary());
                bw.Write(mFileSize);
                bw.Write((int)mOriginalWH.Width);
                bw.Write((int)mOriginalWH.Height);
                long bbPos = stream.Position;
                bw.Write(bitmapBytes);
                {
                    var encoder = new JpegBitmapEncoder();
                    encoder.QualityLevel = JPEG_ENCODER_QUALITY;
                    encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
                    encoder.Save(stream);
                }
                long afterPos = stream.Position;
                bitmapBytes = (int)(afterPos - bbPos - 4); //< 4 == sizeof bitmapBytes
                stream.Seek(bbPos, SeekOrigin.Begin);
                bw.Write(bitmapBytes);
                stream.Seek(afterPos, SeekOrigin.Begin);
                streamPosition = stream.Position;

            }
        }

        public static ThumbnailCacheEntry FromStream(Stream stream, bool bOnlyHeader)
        {
            if (null == stream) {
                throw new ArgumentNullException("stream");
            }
            using (var br = new BinaryReader(stream)) {
                var dateTime = DateTime.FromBinary(br.ReadInt64());
                long fileSize = br.ReadInt64();
                int w = br.ReadInt32();
                int h = br.ReadInt32();
                var originalWH = new Size(w, h);
                int bitmapBytes = br.ReadInt32();
                if (bitmapBytes <= 0 || CACHE_IMAGE_SIZE_MAX < bitmapBytes) {
                    throw new InvalidDataException();
                }
                BitmapImage bitmap = null;
                if (!bOnlyHeader) {
                    byte[] bitmapData = br.ReadBytes(bitmapBytes);
                    using (MemoryStream ms = new MemoryStream(bitmapData)) {
                        bitmap = new BitmapImage();
                        bitmap.BeginInit();
                        bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
                        bitmap.CacheOption = BitmapCacheOption.OnLoad;
                        bitmap.StreamSource = ms;
                        bitmap.EndInit();
                        bitmap.Freeze();
                    }
                    bitmapData = null;
                }
                return new ThumbnailCacheEntry(dateTime, fileSize, bitmap, originalWH);
            }
        }

        public void Clear()
        {
            if (null != bitmapImage) {
                bitmapImage = null;
            }
        }
    }

    public class ThumbnailCache
    {
        private static readonly int FILE_VERSION = 3621610;
        private static readonly string IMAGE_CACHE_FILE_NAME = Path.DirectorySeparatorChar + "TwoPageView3_thumbnailImages.bin";
        private static readonly string CACHE_INDEX_FILE_NAME = Path.DirectorySeparatorChar + "TwoPageView3_thumbnailIndex.txt";
        private string mImageCacheFilePath;
        private string mCacheIndexFilePath;
        private Dictionary<string, long> mIndex = new Dictionary<string, long>();
        private long mLastThumbnailPos = 0;

        struct HitRatioCounter
        {
            public int hit;
            public int miss;
            public int tooOld;

            public void Clear()
            {
                hit = 0;
                miss = 0;
                tooOld = 0;
            }

            public int TotalRequests()
            {
                return hit + miss;
            }
        }
        private HitRatioCounter mHitRatio = new HitRatioCounter();
        private ReaderWriterLock mLock = new ReaderWriterLock();
        private Object mThumbnailReadLock = new Object();
        private Dictionary<string, int> mImageFileNumInFolder = new Dictionary<string, int>();

        public static string ImageFileName
        {
            get { return IMAGE_CACHE_FILE_NAME; }
        }
        public static string IndexFileName
        {
            get { return CACHE_INDEX_FILE_NAME; }
        }

        public ThumbnailCache()
        {
        }

        public void Clear()
        {
            mLock.AcquireWriterLock(Timeout.Infinite);
            mIndex.Clear();
            mImageFileNumInFolder.Clear();
            mLastThumbnailPos = 0;
            mHitRatio.Clear();
            mLock.ReleaseWriterLock();
        }

        public void FileInit()
        {
            Clear();
            mLock.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("D: ThumbnailCache.FileInit() {0}, {1}", mImageCacheFilePath, mCacheIndexFilePath);
            using (FileStream imageFs = new FileStream(mImageCacheFilePath, FileMode.Create, FileAccess.Write)) {
                using (BinaryWriter bw = new BinaryWriter(imageFs)) {
                    bw.Write(FILE_VERSION);
                    mLastThumbnailPos = 4;
                }
            }
            using (FileStream indexFs = new FileStream(mCacheIndexFilePath, FileMode.Create, FileAccess.Write)) {
                using (StreamWriter sw = new StreamWriter(indexFs)) {
                    sw.WriteLine("{0}", FILE_VERSION);
                }
            }
            mLock.ReleaseWriterLock();
        }

        public int NumberOfImageFilesInFolder(string folderPath)
        {
            if (!mImageFileNumInFolder.ContainsKey(folderPath)) {
                return 0;
            }
            return mImageFileNumInFolder[folderPath];
        }

        private void ImageFileNumIncrementByFilePath(string filePath)
        {
            /*
            int slashPos = filePath.LastIndexOf(ActualPath.DirectorySeparatorChar);
            string folderPath;
            if (0 < slashPos) {
                folderPath = filePath.Substring(0, slashPos);
            } else {
                if (2 != filePath.Length) {
                    return;
                }
                System.Text.StringBuilder b = new System.Text.StringBuilder(filePath);
                b.Append(ActualPath.DirectorySeparatorChar);
                folderPath = b.ToString();
            }

            if (mImageFileNumInFolder.ContainsKey(folderPath)) {
                mImageFileNumInFolder[folderPath] = 1 + mImageFileNumInFolder[folderPath];
            } else {
                mImageFileNumInFolder.Add(folderPath, 1);
            }
            if (2 != filePath.Length) {
                ImageFileNumIncrementByFilePath(folderPath);
            }
            */
        }

        public void LoadCachedDataFromFile(string thumbnailFolder)
        {
            Clear();
            mLock.AcquireWriterLock(Timeout.Infinite);
            mImageCacheFilePath = thumbnailFolder + IMAGE_CACHE_FILE_NAME;
            mCacheIndexFilePath = thumbnailFolder + CACHE_INDEX_FILE_NAME;
            try {
                if (!Directory.Exists(thumbnailFolder)) {
                    Directory.CreateDirectory(thumbnailFolder);
                }
                using (FileStream imageFs = new FileStream(mImageCacheFilePath, FileMode.Open, FileAccess.Read)) {
                    using (var br = new BinaryReader(imageFs)) {
                        if (br.ReadInt32() != FILE_VERSION) {
                            throw new IOException();
                        }
                        mLastThumbnailPos = imageFs.Seek(0, SeekOrigin.End);
                    }
                    using (FileStream indexFs = new FileStream(mCacheIndexFilePath, FileMode.Open, FileAccess.Read)) {
                        using (StreamReader sr = new StreamReader(indexFs)) {
                            string line = sr.ReadLine();
                            if (int.Parse(line, System.Globalization.CultureInfo.InvariantCulture) != FILE_VERSION) {
                                throw new IOException();
                            }
                            int nLine = 1;
                            while (null != (line = sr.ReadLine())) {
                                ++nLine;
                                string[] s = line.Split(Path.PathSeparator);
                                if (2 != s.Length || s[0].Length < 3) {
                                    //Console.WriteLine("D: LoadFromFile() skipped line {0}", nLine);
                                    continue;
                                }
                                string path = s[0].Replace(":\\\\", ":\\");
                                long pos = long.Parse(s[1], System.Globalization.CultureInfo.InvariantCulture);
                                if (pos < 4) {
                                    throw new IOException();
                                }
                                if (mIndex.ContainsKey(path)) {
                                    mIndex.Remove(path);
                                    ++mHitRatio.tooOld;
                                } else {
                                    ImageFileNumIncrementByFilePath(path);
                                }
                                mIndex.Add(path, pos);
                            }
                        }
                    }
                }
            } catch (IOException) {
                FileInit();
            } catch (System.ArgumentException) {
                FileInit();
            }
            mLock.ReleaseWriterLock();
            // Console.WriteLine("D: ThumbnailCache.LoadFromFile() loaded {0} entries", index.Count);
        }

        public void Add(string path, ThumbnailCacheEntry entry)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            if (null == entry) {
                throw new ArgumentNullException("entry");
            }
            if (path.StartsWith(TpvPdfDirectoryInfo.PrefixOfTmpFolderPath(), System.StringComparison.Ordinal)) {
                return;
            }
            mLock.AcquireWriterLock(Timeout.Infinite);
            if (mIndex.ContainsKey(path)) {
                var foundEntry = Find(path, entry.DateTime, entry.FileSize);
                if (foundEntry != null)
                {
                    Console.WriteLine("D: ThumbnailCache.Add({0}) already exists.", path);
                    mLock.ReleaseWriterLock();
                    return;
                }
                Console.WriteLine("D: ThumbnailCache.Add({0}) already exists. removing pos={1} and adding {2}", path, mIndex[path], mLastThumbnailPos);
                mIndex.Remove(path);
                ++mHitRatio.tooOld;
            } else {
                ImageFileNumIncrementByFilePath(path);
            }
            mIndex.Add(path, mLastThumbnailPos);
            try {
                using (FileStream fs = new FileStream(mCacheIndexFilePath, FileMode.Append, FileAccess.Write)) {
                    using (StreamWriter sw = new StreamWriter(fs)) {
                        sw.WriteLine("{0}{1}{2}", path, Path.PathSeparator, mLastThumbnailPos);
                    }
                }
                using (FileStream fs = new FileStream(mImageCacheFilePath, FileMode.Append, FileAccess.Write)) {
                    entry.Write(fs, out mLastThumbnailPos);
                }
            } catch (IOException) {
                FileInit();
            }
            mLock.ReleaseWriterLock();
        }

        public bool IfFoundEntry(string path, DateTime dateTime, long fileSize)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            bool bFound = false;
            mLock.AcquireReaderLock(Timeout.Infinite);
            if (!mIndex.ContainsKey(path)) {
                ++mHitRatio.miss;
            } else {
                ++mHitRatio.hit;
                bFound = true;
            }
            mLock.ReleaseReaderLock();
            return bFound;
        }

        public ThumbnailCacheEntry Find(string path, DateTime dateTime, long fileSize)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            mLock.AcquireReaderLock(Timeout.Infinite);
            ThumbnailCacheEntry entry = null;
            if (!mIndex.ContainsKey(path)) {
                ++mHitRatio.miss;
            } else {
                long pos = mIndex[path];
                try {
                    using (FileStream fs = new FileStream(mImageCacheFilePath, FileMode.Open, FileAccess.Read)) {
                        fs.Seek(pos, SeekOrigin.Begin);
                        entry = ThumbnailCacheEntry.FromStream(fs, false);
                        if (entry.DateTime == dateTime && entry.FileSize == fileSize) {
                            ++mHitRatio.hit;
                        } else {
                            entry = null;
                            ++mHitRatio.miss;
                        }
                    }
                } catch (IOException) {
                    FileInit();
                } catch (InvalidDataException) {
                    // thumbnailcache entry data corrupt
                    FileInit();
                } catch (System.ArgumentException) {
                    // element file is corrupted
                    FileInit();
                }
            }
            mLock.ReleaseReaderLock();
            return entry;
        }

        public int CacheFileNumber
        {
            get {
                int rv;
                mLock.AcquireReaderLock(Timeout.Infinite);
                rv = mIndex.Count;
                mLock.ReleaseReaderLock();
                return rv;
            }
        }
        public int CacheSearchRequestNumber
        {
            get {
                int rv;
                mLock.AcquireReaderLock(Timeout.Infinite);
                rv = mHitRatio.TotalRequests();
                mLock.ReleaseReaderLock();
                return rv;
            }
        }
        public int CacheHitNumber
        {
            get {
                int rv;
                mLock.AcquireReaderLock(Timeout.Infinite);
                rv = mHitRatio.hit;
                mLock.ReleaseReaderLock();
                return rv;
            }
        }
        public int TooOldEntryNumber
        {
            get {
                int rv;
                mLock.AcquireReaderLock(Timeout.Infinite);
                rv = mHitRatio.tooOld;
                mLock.ReleaseReaderLock();
                return rv;
            }
        }
    }
}
